﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Net.Sockets;
using System.Threading;
using System.Diagnostics;
using System.Text;
using System.Collections.Generic;

namespace SilverlightPlayground.Net
{
    public class SocketClient : IDisposable
    {
        /// <summary>
        /// Gets or sets the socket.
        /// </summary>
        /// <value>The socket.</value>
        private Socket Socket { get; set; }
        /// <summary>
        /// Gets or sets the end point.
        /// </summary>
        /// <value>The end point.</value>
        public EndPoint EndPoint { get; private set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="SocketClient"/> class.
        /// </summary>
        /// <param name="host">The host.</param>
        /// <param name="port">The port.</param>
        public SocketClient(string host, int port)
            : this(new DnsEndPoint(host, port))
        { }

        /// <summary>
        /// Initializes a new instance of the <see cref="SocketClient"/> class.
        /// </summary>
        /// <param name="address">The address.</param>
        /// <param name="port">The port.</param>
        public SocketClient(IPAddress address, int port)
            : this(new IPEndPoint(address, port))
        { }

        /// <summary>
        /// Initializes a new instance of the <see cref="SocketClient"/> class.
        /// </summary>
        /// <param name="endPoint">The end point.</param>
        public SocketClient(EndPoint endPoint)
        {
#if TRACE
            Debug.WriteLine("SocketClient.ctor(endPoint={0})", endPoint);
#endif

            this.EndPoint = endPoint;
            this.Socket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
        }

        /// <summary>
        /// Handles the Completed event of the Operation control.
        /// </summary>
        /// <param name="sender">The source of the event.</param>
        /// <param name="e">The <see cref="System.Net.Sockets.SocketAsyncEventArgs"/> instance containing the event data.</param>
        void Operation_Completed(object sender, SocketAsyncEventArgs e)
        {
#if TRACE
            Debug.WriteLine("SocketClient.Operation_Completed(e={0})", e.LastOperation);
#endif

            try
            {
                if (e.SocketError != SocketError.Success)
                    throw new SocketFailureException(
                        string.Format("Error in socket operation '{0}'", e.LastOperation))
                    { 
                        ErrorCode = e.SocketError 
                    };

                switch (e.LastOperation)
                {
                    case SocketAsyncOperation.Connect:
                        this.Destroy(e);
                        this.OnConnect();
                        break;
                    case SocketAsyncOperation.Receive:
                        this.OnReceiveChunk(e);
                        break;
                    case SocketAsyncOperation.Send:
                        this.Destroy(e);
                        SocketAction action = e.UserToken as SocketAction ?? SocketAction.Default;
                        this.OnSent(action.Owner, action.Action, action.State);
                        break;
                    default:
                        throw new InvalidOperationException("Unknown status");
                }
            }
            catch(Exception ex)
            {
                this.OnFailure(ex);
            }
        }

        /// <summary>
        /// Destroys the specified e.
        /// </summary>
        /// <param name="e">The <see cref="System.Net.Sockets.SocketAsyncEventArgs"/> instance containing the event data.</param>
        private void Destroy(SocketAsyncEventArgs e)
        {
#if TRACE
            Debug.WriteLine("SocketClient.Destroy(e={0})", e.LastOperation);
#endif

            e.Completed -= new EventHandler<SocketAsyncEventArgs>(Operation_Completed);
            e.Dispose();
        }

        /// <summary>
        /// Connects this instance.
        /// </summary>
        public virtual void Connect()
        {
#if TRACE
            Debug.WriteLine("SocketClient.Connect()");
#endif

            try
            {
                SocketAsyncEventArgs args = new SocketAsyncEventArgs();
                args.RemoteEndPoint = this.EndPoint;
                args.Completed += new EventHandler<SocketAsyncEventArgs>(Operation_Completed);
                this.Socket.ConnectAsync(args);
            }
            catch (Exception ex)
            {
                this.OnFailure(ex);
            }
        }

        /// <summary>
        /// Receives the specified length.
        /// </summary>
        /// <param name="size">The length.</param>
        protected virtual void Receive(long size, string action)
        {
#if TRACE
            Debug.WriteLine("SocketClient.Receive(size={0})", size);
#endif
            this.Receive(size, action, null);
        }

        /// <summary>
        /// Receives the specified size.
        /// </summary>
        /// <param name="size">The size.</param>
        /// <param name="action">The action.</param>
        /// <param name="state">The state.</param>
        protected virtual void Receive(long size, string action, object state)
        {
            this.Receive(this, size, action, state);
        }

        /// <summary>
        /// Receives the specified length.
        /// </summary>
        /// <param name="size">The length.</param>
        protected virtual void Receive(object owner, long size, string action, object state)
        {
#if TRACE
            Debug.WriteLine("SocketClient.Receive(size={0}, state={1})", size, state); 
#endif

            if (this.Socket.Connected)
            {
                byte[] buffer = new byte[size];

                SocketAsyncEventArgs args = new SocketAsyncEventArgs();
                args.UserToken = new SocketAction { Owner = owner, State = state, Action = action };
                args.RemoteEndPoint = this.EndPoint;
                args.Completed += new EventHandler<SocketAsyncEventArgs>(Operation_Completed);
                args.SetBuffer(buffer, 0, buffer.Length);
                this.Socket.ReceiveAsync(args);
            }
            else
                throw new InvalidOperationException("Socket not connected");
        }

        /// <summary>
        /// Receives the line.
        /// </summary>
        /// <param name="action">The action.</param>
        /// <param name="state">The state.</param>
        protected virtual void ReceiveLine(string action, object state)
        {
            this.ReceiveLine(this, action, state);
        }

        /// <summary>
        /// Receives the line.
        /// </summary>
        /// <param name="owner">The owner.</param>
        /// <param name="action">The action.</param>
        /// <param name="state">The state.</param>
        protected virtual void ReceiveLine(object owner, string action, object state)
        {
#if TRACE
            Debug.WriteLine("SocketClient.Receive(size={0}, state={1})", size, state); 
#endif

            if (this.Socket.Connected)
            {
                byte[] buffer = new byte[1];

                SocketAsyncEventArgs args = new SocketAsyncEventArgs();
                args.UserToken = new SocketAction { Owner = owner, State = state, Action = action, LineBuffer = new List<byte>() };
                args.RemoteEndPoint = this.EndPoint;
                args.Completed += new EventHandler<SocketAsyncEventArgs>(Operation_Completed);
                args.SetBuffer(buffer, 0, buffer.Length);
                this.Socket.ReceiveAsync(args);
            }
            else
                throw new InvalidOperationException("Socket not connected");
        }

        /// <summary>
        /// Sends the specified data.
        /// </summary>
        /// <param name="data">The data.</param>
        protected virtual void Send(byte[] data, string action)
        {
#if TRACE
            Debug.WriteLine("SocketClient.Send(data.Length={0})", data.Length);
#endif
            this.Send(data, action, null);
        }

        /// <summary>
        /// Sends the specified data.
        /// </summary>
        /// <param name="data">The data.</param>
        /// <param name="action">The action.</param>
        /// <param name="state">The state.</param>
        protected virtual void Send(byte[] data, string action, object state)
        {
            this.Send(this, data, action, state);
        }

        /// <summary>
        /// Sends the specified data.
        /// </summary>
        /// <param name="data">The data.</param>
        protected virtual void Send(object owner, byte[] data, string action, object state)
        {
#if TRACE
            Debug.WriteLine("SocketClient.Send(data.Lenght={0}, state={1})", data.Length, state); 
#endif

            if (this.Socket.Connected)
            {
                SocketAsyncEventArgs args = new SocketAsyncEventArgs();
                args.UserToken = new SocketAction { State = state, Action = action };
                args.RemoteEndPoint = this.EndPoint;
                args.Completed += new EventHandler<SocketAsyncEventArgs>(Operation_Completed);
                args.SetBuffer(data, 0, data.Length);
                this.Socket.SendAsync(args);
            }
            else
                throw new InvalidOperationException("Socket not connected");
        }

        /// <summary>
        /// Closes this instance.
        /// </summary>
        public virtual void Close()
        {
#if TRACE
            Debug.WriteLine("SocketClient.Close()");
#endif

            this.Socket.Shutdown(SocketShutdown.Both);
            this.Socket.Close();
        }

        /// <summary>
        /// Called when [connect].
        /// </summary>
        protected virtual void OnConnect()
        {
#if TRACE
            Debug.WriteLine("SocketClient.OnConnect()");
#endif
        }

        /// <summary>
        /// Called when [receive].
        /// </summary>
        /// <param name="p">The p.</param>
        protected virtual void OnReceive(object owner, byte[] data, string action, object state)
        {
#if TRACE
            Debug.WriteLine("SocketClient.OnReceive(data.Length={0}, status={1})", data.Length, state);
#endif
        }

        /// <summary>
        /// Raises the <see cref="E:ReceiveChuck"/> event.
        /// </summary>
        /// <param name="args">The <see cref="System.Net.Sockets.SocketAsyncEventArgs"/> instance containing the event data.</param>
        private void OnReceiveChunk(SocketAsyncEventArgs args)
        {
#if TRACE
            Debug.WriteLine("SocketClient.OnReceiveChuck(BytesTransferred={0}, Offset={1}, Length={2}", args.BytesTransferred, args.Offset, args.Buffer.Length); 
#endif

            if (args.BytesTransferred + args.Offset < args.Buffer.Length)
            {
                args.SetBuffer(
                    args.Offset + args.BytesTransferred,
                    args.Buffer.Length - (args.Offset + args.BytesTransferred));
            }
            else
            {
                SocketAction action = args.UserToken as SocketAction ?? SocketAction.Default;

                if (action.LineBuffer == null)
                {
                    this.OnReceive(action.Owner, args.Buffer, action.Action, action.State);
                    this.Destroy(args);
                    return;
                }
                else
                {
                    action.LineBuffer.Add(args.Buffer[0]);

                    if (action.LineBuffer.Count > 1 && action.LineBuffer[action.LineBuffer.Count - 2] == '\r' && action.LineBuffer[action.LineBuffer.Count - 1] == '\n')
                    {
                        this.OnReceive(action.Owner, action.LineBuffer.ToArray(), action.Action, action.State);
                        this.Destroy(args);
                        return;
                    }
                }
            }

            if (this.Socket.Connected)
            {
#if TRACE
                    Debug.WriteLine("Reading next chunk"); 
#endif
                this.Socket.ReceiveAsync(args);
            }
        }

        /// <summary>
        /// Called when [sent].
        /// </summary>
        protected virtual void OnSent(object owner, string action, object state)
        {
#if TRACE
            Debug.WriteLine("SocketClient.OnSent()");
#endif
        }

        /// <summary>
        /// Called when [failure].
        /// </summary>
        /// <param name="ex">The ex.</param>
        protected virtual void OnFailure(Exception ex)
        {
#if TRACE
            Debug.WriteLine("SocketClient.OnFailure(ex={0})", ex);
#endif
        }

        #region IDisposable Members

        /// <summary>
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// </summary>
        public void Dispose()
        {
#if TRACE
            Debug.WriteLine("SocketClient.Dispose()");
#endif

            if (this.Socket != null)
            {
                this.Close();
                this.Socket.Dispose();
            }
        }

        #endregion

        private class SocketAction
        {
            public static SocketAction Default = new SocketAction { Owner = null, Action = "Default", State = null };
            /// <summary>
            /// Gets or sets the sender.
            /// </summary>
            /// <value>The sender.</value>
            public object Owner { get; set; }
            /// <summary>
            /// Gets or sets the action.
            /// </summary>
            /// <value>The action.</value>
            public string Action { get; set; }
            /// <summary>
            /// Gets or sets the state.
            /// </summary>
            /// <value>The state.</value>
            public object State { get; set; }
            /// <summary>
            /// Gets or sets the line buffer.
            /// </summary>
            /// <value>The line buffer.</value>
            public List<byte> LineBuffer { get; set; }
        }
    }
}
